package tests;
/* --- THE OCEAN WORLD -----------------------------------------------
Fish swim across the screen from right to left. There may be one fish,
or a whole school of fish, or none at a time. A hungry shark swims in
place at the left side, moving only up and down, controlled by the
"up" and "down" keys. It tries to catch and eat the fish. 
It gets bigger with each fish it eats, it keeps getting hungrier
and hungrier as the time goes on between the catches. 
It dies of starvation, if not fed in time.
--------------------------------------------------------------------*/

import colors.*;
import draw.*;
import geometry.*;
import java.util.Random;
import tester.*;


/**
 * Copyright 2007, 2008 Viera K. Proulx
 * This program is distributed under the terms of the 
 * GNU Lesser General Public License (LGPL)
 */

/*
+----------+
| Shark    |
+----------+
| Posn loc |
| int life |
+----------+

*/

// to represent a shark in an ocean world
class Shark {
  Posn loc;
  int life;
  int MaxLIFESPAN = 220;

  Shark(Posn loc, int life) {
    this.loc = loc;
    this.life = life;
  }
  
  // produce a new shark from this shark moved in response to the key press
  Shark onKeyEvent(String ke){
    if (ke.equals("up"))
      return new Shark(new Posn(this.loc.x, this.loc.y - 3), this.life);
    else if (ke.equals("down"))
      return new Shark(new Posn(this.loc.x, this.loc.y + 3), this.life);
    else
      return this;
  }
  
  // draw this shark on the given Canvas
  boolean draw(Canvas c){
    return c.drawDisk(this.loc, this.life / 10 + 2, new Black()) &&
           c.drawDisk(new Posn(this.loc.x + 2, this.loc.y), 1, new Red()) &&
           c.drawLine(this.loc, 
                      new Posn(this.loc.x + 25, this.loc.y), 
                      new Black());
  }
  
  // produce a shark hungrier by a minute than this one
  Shark onTick(){
    if (this.life > 0)
      return new Shark(this.loc, this.life - 1);
    else
      return this;
  }
    
  // produce a shark after this one ate one fish
  // for now every fish tastes the same
  // later, fish nutrition value may be determined by the size or the color 
  Shark getFatter(){
    return new Shark(this.loc, Math.min(this.life + 30, this.MaxLIFESPAN));
  }
  
  // is this shark dead?
  boolean isDead(){
    return this.life <= 0;
  }
  
}

/*
+-----------------+
| Fish            |
+-----------------+
| Posn loc        |
| boolean escaped |
+-----------------+

*/

// to represent a fish in an ocean world
class Fish {
  Posn loc;
  boolean hasEscaped;
  int speed;

  Fish(Posn loc, boolean hasEscaped, int speed) {
    this.loc = loc;
    this.hasEscaped = hasEscaped;
    this.speed = speed;
  }
  
  // produce from this fish the one that swam for a minute more
  Fish onTick(){
    if (hasEscaped)
      return this.makeFish(200, 200);
    else if (this.loc.x < 0)
      return new Fish(this.loc, true, this.speed);
    // keep the fishy inbounds - refactor this!!!!
    else if (this.loc.y < 0)
      return new Fish(new Posn(this.loc.x, - this.loc.y), false, this.speed);
    else if (this.loc.y > 200)
      return new Fish(new Posn(this.loc.x, this.loc.y % 200), false, this.speed);
    else
      return new Fish(new Posn(this.loc.x - this.speed, 
                               this.loc.y + 4),
                               false, this.speed);
  }
  
  // has this fish escaped?
  boolean escaped(){
    return (this.loc.x < 0) || this.hasEscaped;
  }
  
  // is this fish a food for the given shark?
  boolean isFood(Shark shark){
    return
      Math.sqrt((this.loc.x - shark.loc.x) * (this.loc.x - shark.loc.x) +
                (this.loc.y - shark.loc.y) * (this.loc.y - shark.loc.y)) < 25;     
  }

  // draw this fish on the given Canvas
  boolean draw(Canvas c){
    return c.drawDisk(this.loc, 10, new Red()) &&
           c.drawDisk(new Posn(this.loc.x - 8, this.loc.y), 4, new Red()) &&
           c.drawDisk(new Posn(this.loc.x - 6, this.loc.y), 1, new Yellow()) &&
           c.drawRect(new Posn(this.loc.x + 9, this.loc.y - 4), 4, 8, new Red());
  }
  
  // produce a new fish at the right quarter of the canvas at random height
  Fish makeFish(int WIDTH, int HEIGHT){
    return new Fish(new Posn(180, this.loc.y), false, this.speed);
  }
    
  // produce a random initial coordinate of the fish
  int randomLimited(int LIMIT){
    return Math.abs(new Random().nextInt()) % LIMIT;
  }
  
  // replace this fish with a new one
  Fish feedShark(Shark shark){
    return this.makeFish(200, 200); 
  }
}

/*
+-------------+
| OceanWorld  |
+-------------+
| Shark shark |
| Fish fish   |
+-------------+

*/


/*
            +---------+                
            | ILoFish |<--------------+
            +---------+               |
            +---------+               |
                 |                    |
                / \                   |
                ---                   |
                 |                    |
       -------------------            |
       |                 |            |
  +----------+    +--------------+    |
  | MtLoFish |    | ConsLoFish   |    |
  +----------+    +--------------+    |
  +----------+    | Fish first   |    |
                  | ILoFish rest |-+  |
                  +--------------+ |  |
                                   |  |
                                   +--+

*/

// to represent a school of fish
interface ILoFish {
 
   // produce from this fish the one that swam for a minute more
  ILoFish onTick();

  // is there a fish in this school that can be eaten by the given shark?
  boolean isFood(Shark shark);

  // produce a school of fish with one eaten and replaced by a new random one
  ILoFish feedShark(Shark shark);
  
  // draw this fish on the given Canvas
  boolean draw(Canvas c);
  
}

// to represent an empty school of fish
class MtLoFish implements ILoFish {

  MtLoFish() {
  }

   // produce from this fish the one that swam for a minute more
  public ILoFish onTick(){
    return this;
  }

  // is there a fish in this school that can be eaten by the given shark?
  public boolean isFood(Shark shark){
    return false;
  }

  // produce a school of fish with one eaten and replaced by a new random one
  public ILoFish feedShark(Shark shark){
    return this;
  }

  // draw this fish on the given Canvas
  public boolean draw(Canvas c){
    return true;
  }
}

// to represent a nonempty school of fish
class ConsLoFish implements ILoFish {
  Fish first;
  ILoFish rest;

  ConsLoFish(Fish first, ILoFish rest) {
    this.first = first;
    this.rest = rest;
  }

   // produce from this school of fish the one where all swam for a minute more
  public ILoFish onTick(){
    if (this.first.escaped())
      return new ConsLoFish(this.first.makeFish(200, 200),
                            this.rest.onTick());
    else
      return new ConsLoFish(this.first.onTick(),
                          this.rest.onTick());
  }
  
  // is there a fish in this school that can be eaten by the given shark?
  public boolean isFood(Shark shark){
    return this.first.isFood(shark) || this.rest.isFood(shark);
  }

  // produce a school of fish with one eaten and replaced by a new random one
  public ILoFish feedShark(Shark shark){
    if (this.first.isFood(shark))
      return new ConsLoFish(this.first.makeFish(200, 200), this.rest);
    else 
      return new ConsLoFish(this.first, this.rest.feedShark(shark));
  }

  // draw this fish on the given Canvas
  public boolean draw(Canvas c){
    return this.first.draw(c) && this.rest.draw(c);
  }
}

/*
The world includes a school of fish. When a fish is eaten, a new fish appears
on the right hand side. The shark grows by a fixed amount, but the growth is
limited by a fixed upper bound. Shark's display reflects its growth.
Every fish is given a new random speed when it appears, it keeps swimming
at the same speed until either eaten or it escapes.
*/
// to represent an ocean world
class OceanWorld extends World{
  Shark shark;
  ILoFish fish;
  int WIDTH =  200;
  int HEIGHT = 200;

  OceanWorld(Shark shark, ILoFish fish) {
    this.shark = shark;
    this.fish = fish;
  }
  
  // start the world and the timer
  boolean go() { return this.bigBang(200, 200, 0.06); }

  // produce a new OceanWorld after one minute elapsed: 
  // move the fish, starve the shark, check if the fish is eaten or has escaped
  public World onTick(){
    // if the shark found fish, fed the shark, replace the fish with a new one                        
    if (this.fish.isFood(this.shark))
      return new OceanWorld(this.shark.getFatter(),
                            this.fish.feedShark(shark));
                            
    // if the shark starved to death, end the world
    else if(this.shark.isDead())
      return this.endOfWorld("the shark starved to death");
    
    // no special events, just move the fish and starve the shark 
    else 
      return new OceanWorld(this.shark.onTick(), this.fish.onTick());
  }
  
  // produce a new OceanWorld in response to the given key press
  public World onKeyEvent(String ke){
    return new OceanWorld(this.shark.onKeyEvent(ke), this.fish);
  }
  
  // draw this world
  public boolean draw(){
    return this.theCanvas.drawRect(new Posn(0, 0), 
                            this.WIDTH, this.HEIGHT, new Blue()) &&
           this.fish.draw(this.theCanvas) &&
           this.shark.draw(this.theCanvas);
  }
 
}

// Examples of the classes Fish, Shark, OceanWorld and tests for all methods
class Examples{
  Examples(){}

  Shark mac = new Shark(new Posn(10, 100), 200);
  Shark mac2 = new Shark(new Posn(10, 100), 100);
  Shark mac3 = new Shark(new Posn(10, 100), -1);

  Fish fishy = new Fish(new Posn(190, 100), false, 8);
  Fish yummy = new Fish(new Posn(15, 98), false, 8);
  Fish gone = new Fish(new Posn(15, 98), true, 5);
  Fish gone2 = new Fish(new Posn(-2, 98), false, 5);
  
  Fish fishy1 = new Fish(new Posn(190, 160), false, 6);
  Fish fishy2 = new Fish(new Posn(190, 120), false, 5);
  Fish fishy3 = new Fish(new Posn(190, 140), false, 4);
  Fish fishy4 = new Fish(new Posn(190, 100), false, 8);
  Fish fishy5 = new Fish(new Posn(190, 180), false, 9);
  void printFishies(){
    System.out.println("fishy1: (" + fishy1.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy2: (" + fishy2.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy3: (" + fishy3.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy4: (" + fishy4.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy5: (" + fishy5.loc.x + ", " + fishy1.loc.y + ")");
  }
  
  
  ILoFish nofish = new MtLoFish();
  ILoFish fishies = new ConsLoFish(fishy1,
                    new ConsLoFish(fishy2,
                    new ConsLoFish(fishy3,
                    new ConsLoFish(fishy4,
                    new ConsLoFish(fishy5, new MtLoFish())))));
                    
  ILoFish manyfish = new ConsLoFish(fishy,
                     new ConsLoFish(yummy, new MtLoFish()));
                    
    
  OceanWorld sfw = new OceanWorld(this.mac, this.fishies); 
  
  Canvas c = new Canvas(200, 200);
  
  // test the method onKeyEvent in the class Shark
  @TestMethod
  boolean testSharkOnKeyEvent(Tester t){
    return
      t.checkExpect(this.mac.onKeyEvent("left"), this.mac) && 
      t.checkExpect(this.mac.onKeyEvent("up"), 
        new Shark(new Posn(10, 97), 200)) && 
        t.checkExpect(this.mac.onKeyEvent("down"), 
        new Shark(new Posn(10, 103), 200));
  }
  
  // a visual test for drawing a Shark
  boolean drawShark(){
    return c.show() && this.mac.draw(c);
  }
  
  // test the method onTick in the class Shark
  @TestMethod
  boolean testSharkOnTick(Tester t){
    return
      t.checkExpect(this.mac.onTick(),
          new Shark(new Posn(10, 100), 199)) && 
      t.checkExpect((new Shark(new Posn(10, 100), 0)).onTick(),
          new Shark(new Posn(10, 100), 0)); 
  }  
  
  // test the method getFatter in the class Shark
  @TestMethod
  boolean testGetFatter(Tester t){
    return t.checkExpect(mac.getFatter(),
        new Shark(new Posn(10, 100), 220)) &&
           t.checkExpect(mac2.getFatter(),
               new Shark(new Posn(10, 100), 130));
  }

  // test the method isDead in the class Shark
  @TestMethod
  boolean testIsDead(Tester t){
    return t.checkExpect(mac.isDead(), false) &&
           t.checkExpect(mac3.isDead(), true);
  }


  // test the method onTick in the class Fish
  @TestMethod
  boolean testFishOnTick(Tester t){
    return 
      t.checkExpect(this.fishy.onTick(), 
          new Fish(new Posn(182, 104), false, this.fishy.speed)) &&
      t.checkExpect((new Fish(new Posn(-2, 100), true, 10)).onTick(), 
          new Fish(new Posn(180, 100), false, 10)) && 
      t.checkExpect((new Fish(new Posn(-2, 100), false, 8)).onTick(), 
          new Fish(new Posn(-2, 100), true, 8)); 
  }
  
  
  // test the method escaped in the class Fish
  @TestMethod
  boolean testFishEscaped(Tester t){
    return 
      t.checkExpect(this.fishy.escaped(), false) && 
      t.checkExpect((new Fish(new Posn(-2, 100), true, 8)).escaped(), true) &&
      t.checkExpect((new Fish(new Posn(-2, 100), false, 8)).escaped(), true); 
  }

  // test the method isFood in the class Fish
  @TestMethod
  boolean testFishIsFood(Tester t){
    return 
      t.checkExpect(this.fishy.isFood(this.mac), false) &&
      t.checkExpect(this.yummy.isFood(new Shark(new Posn(10, 100), 20)), true);
  } 

  // a visual test for drawing a fish
  boolean drawFish(){
    return c.show() && this.fishy.draw(c);
  }
  
  
  // test for the method randomLimited in the class Fish
  @TestMethod
  boolean testRandomLimited(Tester t){
    return t.checkExpect(this.fishy.randomLimited(5) < 6 && 
                  this.fishy.randomLimited(5) >= 0,
                  true);
  }

  // test the method makeFish in the class Fish
  @TestMethod
  boolean testMakeFish(Tester t){
    return t.checkExpect(this.fishy.makeFish(200, 200).hasEscaped, false) &&
           t.checkExpect(this.fishy.makeFish(200, 200).loc.y < 200, true) &&
           t.checkExpect(this.fishy.makeFish(200, 200).loc.y >=  0, true) &&
           t.checkExpect(this.fishy.makeFish(200, 200).loc.x < 200, true) &&
           t.checkExpect(this.fishy.makeFish(200, 200).loc.x >= 150, true) &&
           t.checkExpect(this.fishy.makeFish(200, 200).speed <  10, true) &&
           t.checkExpect(this.fishy.makeFish(200, 200).speed >   1, true);
  }
 

  ILoFish manyfishNext = new ConsLoFish(fishy.onTick(),
                         new ConsLoFish(yummy.onTick(), new MtLoFish()));
  ILoFish manyfishEaten = new ConsLoFish(fishy,
                          new ConsLoFish(yummy.feedShark(this.mac), new MtLoFish()));

  // test the method onTick in the classes that represent a school of fish
  @TestMethod
  boolean testILoFishOnTick(Tester t){
    return t.checkExpect(this.nofish.onTick(), this.nofish) &&
           t.checkExpect(this.manyfish.onTick(), this.manyfishNext);
  }

  // test the method isFood in the classes that represent a school of fish
  @TestMethod
  boolean testILoFishIsFood(Tester t){
    return t.checkExpect(this.nofish.isFood(this.mac), false) &&
           t.checkExpect(this.manyfish.isFood(this.mac), true) &&
           t.checkExpect(this.fishies.isFood(this.mac), false);
  }

  // test the method feedShark in the classes that represent a school of fish
  @TestMethod
  boolean testILoFishFeedShark(Tester t){
    return t.checkExpect(this.nofish.feedShark(this.mac), this.nofish) &&
           t.checkExpect(this.manyfish.feedShark(this.mac), this.manyfishEaten) &&
           t.checkExpect(this.fishies.feedShark(this.mac), this.fishies);
  }

  // a visual test for drawing a school of fish
  boolean drawFishies(){
    return c.show() && this.manyfish.draw(c);
  }

  // test the method onKeyEvent in the class OceanWorld
  @TestMethod
  boolean testOceanOnKeyEvent(Tester t){
    return
      t.checkExpect(this.sfw.onKeyEvent("left"), 
          new OceanWorld(this.mac, this.fishies)) && 
      t.checkExpect(this.sfw.onKeyEvent("up"),
        new OceanWorld(new Shark(new Posn(10, 97), 200), this.fishies)) && 
      t.checkExpect(this.sfw.onKeyEvent("down"), 
        new OceanWorld(new Shark(new Posn(10, 103), 200), this.fishies));
  }
  
  OceanWorld sfwEnd = new OceanWorld(this.mac3, this.fishies);
  OceanWorld sfwReg = new OceanWorld(this.mac, this.fishies);
  OceanWorld sfwYum = new OceanWorld(this.mac, this.manyfish);
  
  // test the method onTick in the class OceanWorld
  @TestMethod
  boolean testOceanOnTick(Tester t){
    return
      sfwReg.go() &&
      // world goes on
      t.checkExpect(this.sfwReg.onTick(),
          new OceanWorld(this.mac.onTick(), this.fishies.onTick())) &&
        
      // fish is eaten, shark is fatter, new fish appears
      t.checkExpect(this.sfwYum.onTick(), 
          new OceanWorld(this.mac.getFatter(), 
                              this.manyfishEaten));
      // world ends - the shark starves to death --- fails due to randomness
      // cannot invoke method in the teachpack - no test is possible
      // (check this.sfwEnd.onTick() 
      //    expect this.sfw.endWorld("The shark has starved to death"));
  } 
  
  public static void main(String[] args){
	  Tester.run(new Examples());
  }
  
/*
  // a visual test for drawing the OceanWorld - cannot be run
  boolean drawOcean(){
    return this.sfw.draw();
  }
*/
}
